react 源码开始的那一步

March 24, 2021

前言

本来想着学习路径是从 react 周围生态开始的,比如之前的 preact,react-router,再到后面 immutable.js,读懂这些源码,接着可以看 dva,rematch,亦或则是 redux,甚至是 ant-design 都可以看看的,到了最后再吃大餐,react 源码。只是不知道为什么想要挑战一下自己,不想这么循循渐进。想要试试自己的实力。于是便有了这次的 react 源码的阅读。

刚开始读的时候觉得看 react 源码是一种享受,就像在读一本小说一样,惊险刺激,停不下来。只是后面到了 fiber 的阶段,就有点懵逼了,这绝对是烧脑侦探片,而我是里面的路人甲,看几行代码都觉得费劲。这个时候遇到了如何阅读大型前端开源项目的源码,文章写得极好,根据上面的内容开始去看 react 文档里面的 Contribution Guide 里面的指导,随后又读了读 Blog 部分,简直是 amazing,尤其是Beyond React 16 by Dan Abramov,以及A Cartoon Intro to Fiber 。看得内心澎湃,觉得为何自己不能早点入坑呢?随后有看了正妹,以及方大神的介绍,顿时有了不少底气。

学习方式

想要完整的学习,于是最简单的从 ReactDom.render 开始一步一步往下走,后面遇到看不懂的地方,则开始用 debug 的方式,打断点看代码。按照 Contribution 里面的意思,先安装包,然后构建项目,生成对应的 core、dome 文件。这里需要注意的是,构建 React 项目居然要安装 Java,而且只能用 yarn,嗯,还是自己家的东西好是吧。构建好文件后,复制并打开fixtures/packaging/babel-standalone/dev.htm 文件,就可以是愉快的调试了(后期看代码看的心烦都是靠 debug 走下来的)。由于还有很多地方没有去读或者没有读懂,这里只是作为学习的记录,记录的是上面 dev.htm 里面这个例子的加载而已。

ReactDOM.render(
  <h1>Hello World!</h1>,
  document.getElementById('container')
);

就是从上面的例子,一步一步走下去,直到没有下一步。就是本文介绍的内容。由于涉及到的步骤内容较多,所以采用思维导图的方式来介绍。

前部分

这里是初始化以及 Reconciler 和 Scheduler 的前部分。

从 ReactDom.render 里面过来时,会先创建 new ReactRoot(),该对象也就是下图左侧的 root。同时 root.current 为 fiber 对象。而 fiber.stateNode 指回 root。在 updateContainer 函数里面会计算出超时时间 expirationTime,这个时间常数在后面经常用上。

前面这部分主要功能是以建立 root 对象,并创建第一个 fiber,HostRoot,也就是 tag 为 3 的情况。这个 HostRoot 有点类似于上文中的 container,将会包含的子 fiber,并且以后的 dom 节点的操作都少不了 HostRoot。

fiber 就是一个普通的对象,对于这个对象而言,最重要的字段是下面这几个:

const fiber = {
  stateNode: root,
  return: null,
  child: null,
  sibling: null,
  tag: HostRoot,
  effectTag: Callback,
  expirationTime: 1,
  updateQueue: updateQueue,
  alternate: workInProgress,
}

stateNode 在本文中就是 dom 节点,或者是 root;return 和 child 分别指向 父 Fiber 以及子 fiber,sibling 则是兄弟 fiber 了,这四个字段构成了 fiber 间最直接的关系;

tag 代表当前 fiber 类型,目前有 17 个值;effectTag 有 14 个类型,按照 bitmap 的结构,表示的是 dom 操作类型。

上图中 performWork 之前会构建一个 updateQueue,正如其名一个更新队列。在图中可以看到,root.containerInfo 为 container 这个 dom 元素,而传入 ReactDOM.render 的第一个元素,则在 update 对象的 payload 上。这里有两个重要函数 scheduleWork 和 performWork,可以说是开始 react 工作的第一步,其简化大致如下:

function scheduleWork(fiber, expirationTime) {
  let node = fiber;
  while(node !== null) {
    if(node.return === null) {
      const root = node.stateNode;
      const rootExpirationTime = root.expirationTime;
      requestWork(root, rootExpirationTime);
    }
    node = node.return;
  }
}
function requestWork(root, expirationTime) {
  if (expirationTime === Sync) {
    performWork(Sync, null);
  }
}
function performWork(minExpirationTime, dl) {
  findHighestPriorityRoot();
  while(nextFlushedRoot !== null) {
    performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, false);
    findHighestPriorityRoot();
  }
}

代码中的 Sync = 1,也就是本文中 expirationTime 的值。这里面 scheduleWork 是更新的开始,通过循环找到,也就是 return 字段,沿着父 fiber 的路径一直到根节点。最后在根 root 开始工作啦。而 performWork ,这名字取得真好,先是 findHighestPriorityRoot,获取当前的最高优先的 root,再 nextFlushedRoot = root,执行 performWorkOnRoot 函数,再循环 findHighestPriorityRoot 函数会更新 nextFlushedRoot,但是本文中,只会发生一次循环,只有一个 root 呀。。至于其他情况,还不晓得,当然好像不重要。performWorkOnRoot 函数如下:

function performWorkOnRoot(root, expirationTime, isYieldy) {
  if (!isYieldy) {
    let finishedWork = root.finishedWork;
    if(finishedWork === null) {
      root.finishedWork = null;
      renderRoot(root, false);
      finishedWork = root.finishedWork;
      if (finishedWork !== null) {
        completeRoot(root, finishedWork, expirationTime);
      }
    }
  }
}

performWorkOnRoot 函数主要是通过是否异步 isYield,有的话就进入 commit 阶段,上面的代码自然是没有的部分,没有就进入 renderRoot,等 renderRoot 结束了再判断有没有完成,完成就进入 completeRoot 函数,也就是 commit 阶段咯。下文中的大部分步骤都是在 renderRoot 里面执行的。

workInProgress tree

上面的过程更多只是准备以及刚进入更新的过程,下面则是 reconciler 的核心部分。 fiber 是有两个阶段,Phase 1 render/reconcilation,在这个阶段是生成更新 fiber,更新虚拟 DOM 的过程,这个过程是可以被打断的。第二个阶段是 commit 阶段,这个阶段里面会把元素插入更新删除到 dom 树里面,无法被打断。本段落以及前面段落都是在 Phase 1 里面。

该阶段最重要的一个特征是会创建一个 workInProgress tree。在之前已经创建了一个父子兄关联的 fiber tree 了,而本次过程里面会再次创建一个类似的 tree。如下图所示:

可以看出来在上图的 root 下面,current fiber 下面有两个 fiber 构成父子关系。先看看进入 renderRoot 的大致写法:

function renderRoot(root, isYieldy) {
  nextUnitOfWork = createWorkInProgress(
    nextRoot.current,
    null,
    nextRenderExpirationTime,
  );
  do {
    workLoop(isYieldy);
    break;
  } while(true)
}
function workLoop(isYieldy) {
  if (!isYieldy) {
    while (nextUnitOfWork !== null) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
  }
}
function performUnitOfWork(workInProgress) {
  const current = workInProgress.alternate;
  let next;
  next = beginWork(current, workInProgress, nextRenderExpirationTime);
  if(next == null) {
    next = completeUnitOfWork(workInProgress);
  }
  return next;
}

上面可以看到是由两个循环组成的,而在进入循环之前,会 创建一个 fiber,也就是 workInProgress tree 的 root fiber,即 HostRoot。这个 HostRoot 与 root.current 的关系更像是一个浅复制的关系,共享一个 stateNode,tag 都为 HostRoot。 workLoop 里面的 nextUnitOfWork 全局变量指的是下次要处理的 fiber 单元,自然首次是 workInProgress tree 的根元素,而下次则是该根 fiber 的子 fiber,也就是 child,不断下来从而实现 tree 的迭代。在工作单元 performUnitOfWork 函数里面,有个至关重要的函数 beginWork,顾名思义要开始工作了,前面函数更多的只是一个展开迭代,beginWork 才是阶段一里面最为重要的部分。

beginWork

function beginWork(current, workInProgress, renderExpirationTime) {
  switch (workInProgress.tag) {
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderExpirationTime);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderExpirationTime);
  }
}
function updateHostRoot(current, workInProgress, renderExpirationTime) {
  let updateQueue = workInProgress.updateQueue;
  processUpdateQueue(
    workInProgress,
    updateQueue,
    nextProps,
    null,
    renderExpirationTime,
  );
  reconcileChildren(current, workInProgress, nextChildren);
  return workInProgress.child;
}
function reconcileChildren(current, workInProgress, nextChildren, renderExpirationTime) {
  workInProgress.child = reconcileChildFibers(
    workInProgress,
    current.child,
    nextChildren,
    renderExpirationTime,
  );
}

beginWork 里面根据不同的 fiber 类型来选定更新函数。updateHostRoot 则是更新 HostRoot,其先是浅复制了 current.updateQueue,再修改当前 workInProgress.effectTag = 32。更新队列的时候会把之前的 firstUpdate/lastUpdate 置为 null,同时 firstEffect/lastEffect 指向 update。

这里的 nextChildren 是就是 update 里面的 payload 的 element,也就是传入 ReactDom.render 的 element。通过 reconcileChildren,会直接的创建一个子 fiber,并返回到 workInProgress.child。由于 element 的 type 为 'h1',所以该 child 的 tag 为 HostComponent,child.effectTag = Placement是一个需要插入元素的 fiber。这个 effectTag 在后面也会用到。

生成 child 返回给到 workInProgress.child,也就是下一轮的 nextUnitOfWork。child 也就是下一个工作单元 fiber 了。updateHostComponent 函数同样也会进入 reconcileChildren 里面,只是并不会生成一个 fiber 传给 child.child,因为该 child fiber 没有任何的子元素,所以直接结束,

completeWork

上面结束后,会继续在 performUnitOfWork 执行 completeUnitOfWork,上一段的工作主要是建立 workInProgress tree,而这一段的工作将是生成 DOM。先看看 completeUnitOfWork:

function completeUnitOfWork(workInProgress) {
  while (true) {
    const returnFiber = workInProgress.return;
    const siblingFiber = workInProgress.sibling;
    if ((workInProgress.effectTag & Incomplete) === NoEffect) {
      // 这个fiber 已经完成
      let next = completeWork(
        current,
        workInProgress,
        nextRenderExpirationTime,
      );
      // 修改 firstEffect/lastEffect 为当前 workInProgress 也就是 child fiber
      if (siblingFiber !== null) {
        return siblingFiber;
      } else if (returnFiber !== null) {
        workInProgress = returnFiber;
        continue;
      } else {
        // 到达根部
        return null;
      }
    }
  }
}

这里 completeUnitOfWork 的传参是 child fiber,先是执行 completeWork 函数,随后开始循环,如果有兄弟 siblingFiber 则用,否之则为父 fiber,如果都没有则 return null。可想而知结束该循环的方式就是循环执行到 workInProgress tree 的根部。值得注意的是这里的 子 fiber 的 effects 会通过链表的形式被添加到父 fiber 的 effects 上面

if (returnFiber.firstEffect === null) {
  returnFiber.firstEffect = workInProgress.firstEffect;
}
if (workInProgress.lastEffect !== null) {
  if (returnFiber.lastEffect !== null) {
    returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
  }
  returnFiber.lastEffect = workInProgress.lastEffect;
}

传递方式如上面所示,returnFiber 就是父 fiber,如果有 returnFiber.lastEffect 以及 workInProgress.lastEffect,则通过 nextEffect 这个字段来传递,当然啦这里的 lastEffect/nextEffect 都是 fiber 元素哦。

接下来看看重点 completeWork

function completeWork(current, workInProgress, renderExpirationTime) {
  const newProps = workInProgress.pendingProps;
  const type = workInProgress.type;
  switch (workInProgress.tag) {
    case HostComponent: {
      if(current === null || workInProgress.stateNode === null) {
        let instance = createInstance(
          type,
          newProps,
          // div#container 元素
          rootContainerInstance,
          currentHostContext,
          workInProgress,
        );
        appendAllChildren(instance, workInProgress);
        workInProgress.stateNode = instance;
        return null;
      }
    }
  }
}

在 completeWork 里会通过 createInstance 创建 dom 元素,这里的 instance 就是 <h1></h1> 随后会进一步添加 Dom 属性,最后成为 <h1>Hello World!<h1>。传递到 workInProgress 也就是 child fiber 的 stateNode。如上图所示。返回 null, completeUnitOfWork 里面会继续 completeWork 循环,此时 workInProgress.tag = HostRoot,进入不一样的 case,但是好像没有什么执行的。。。继续返回 null,同时和第一轮一样也会清空 fiber.expirationTime。

到了这一步基本就完成 completeUnitOfWork 的工作,接着会退出 performUnitOfWork 循环回到上文的第一个图的 performWorkOnRoot 里面,执行后面 performWorkOnRoot 函数。也就是 commit 阶段。

commit

commit 阶段主要有三大循环,每个循环都有不同的作用,其简化如下所示

function commitRoot(root, finishedWork) {
  let firstEffect;
  finishedWork.lastEffect.nextEffect = finishedWork;
  firstEffect = finishedWork.firstEffect;

  nextEffect = firstEffect;
  while(nextEffect !== null) {
    commitBeforeMutationLifecycles()
  }

  nextEffect = firstEffect;
  while(nextEffect !== null) {
    commitAllHostEffects()
  }

  nextEffect = firstEffect;
  while(nextEffect !== null) {
    commitAllLifeCycles(root, currentTime, committedExpirationTime);
  }
}

第一个 commitBeforeMutationLifecycles 函数,主要是执行组件的 getSnapshotBeforeUpdate 方法,这也是 react 新增加的一个生命钩子,该函数的返回值 snapshot,将是 componentDidUpdate 的第三个传参,commitBeforeMutationLifecycles 的主要作用也就在于此。

第二个 commitAllHostEffects 函数。这里面会将之前的插入,更新,删除和 ref 卸载的操作都执行到真实 DOM 上面。

function commitAllHostEffects() {
  const effectTag = nextEffect.effectTag;
  if (effectTag & ContentReset) {
    // ...
  }
  if (effectTag & Ref) {
    // ...
  }
  let primaryEffectTag = effectTag & (Placement | Update | Deletion);
  switch (primaryEffectTag) {
      case Placement: {
        commitPlacement(nextEffect);
        nextEffect.effectTag &= ~Placement;
        break;
      }
  }
  nextEffect = nextEffect.nextEffect;
}
function commitPlacement(finishedWork) {
  const parentFiber = getHostParentFiber(finishedWork);
  switch (parentFiber.tag) {
    case HostRoot:
      parent = parentFiber.stateNode.containerInfo;
      isContainer = true;
      break;
  }
  let node = finishedWork;
  while(true) {
    if (node.tag === HostComponent || node.tag === HostText) {
      if (isContainer) {
        appendChildToContainer(parent, node.stateNode);
      }
    }
    if (node === finishedWork) {
      return;
    }
  }
}

可知当前的 nextEffect 是 child, 在 commitAllHostEffects 里面根据不同的场景处理,由于都是 bitmap,所以流程下来很简单。最终由于 child 的 effectTag 为 Placement,从而 找到 root 的 containerInfo,将 child 的 stateNode 添加到 containerInfo 里面,并随后清楚掉 child.effectTag 的 Placement 位置。这样就从肉眼上可以看到的真实的 DOM 结构被改变了。第二轮 commitAllHostEffects 循环的时候,由于父 fiber 的 effectTag 为 Callback,不存在任何进一步的操作,最后会退出本次循环。

第三个循环 commitAllLifeCycles 函数,如下

function commitAllLifeCycles(finishedRoot, currentTime, committedExpirationTime) {
  while (nextEffect !== null) {
    const effectTag = nextEffect.effectTag;
    if (effectTag & (Update | Callback)) {
      const current = nextEffect.alternate;
      commitLifeCycles(
        finishedRoot,
        current,
        nextEffect,
        currentTime,
        committedExpirationTime,
      );
    }
    if (effectTag & Ref) {
      // .. 处理 ref 相关
    }
    const next = nextEffect.nextEffect;
    nextEffect.nextEffect = null;
    nextEffect = next;
  }
}
function commitLifeCycles(
  finishedRoot, 
  current, 
  finishedWork, 
  currentTime, 
  committedExpirationTime
) {
  switch (finishedWork.tag) {
    case ClassComponent: {
      // 执行生命钩子,componentDidMount与componentDidUpdate
    }
    case HostRoot: {

    }
    case HostComponent: {

    }
  }
}

在上面的 commitAllLifeCycles 函数中,通过 commitLifeCycles 方法,执行生命钩子 componentDidMount 与 componentDidUpdate 的调用,同时会处理 updateQueue。这两点应该就是该循环的主要作用了。

通过上面的三个循环,而不是递归的方式实现了 commit 阶段。最后执行回归到 performWorkOnRoot,并结束前面两个循环。到此结束了。

总结

本文更多的只是从一个非常非常简单的例子来摸索 react 的首次渲染,能够清晰的看到其生成的 workInProgress tree,以及 reconciliation 与 commit 两个阶段的存在。一切的真实的 DOM 操作都发生在 commit 阶段,同时也会执行相关的生命钩子。但是对于 react 而言以上的探索是远远不够的。后面还会继续其他研究如:

  1. 后台调度机制 requestIdleCallback 的 ployfill 实现,以及现场保护等等运用。
  2. diff 机制,原则。
  3. 组件更新过程。
  4. 其他生命周期过程。

参考

开头文章列出的部分

  1. 如何阅读大型前端开源项目的源码
  2. Beyond React 16 by Dan Abramov
  3. A Cartoon Intro to Fiber
  4. React Fiber架构
  5. 为 Luy 实现 React Fiber 架构